{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "84369724",
   "metadata": {},
   "source": [
    "# DAY 21\n",
    "\n",
    "常见的降维算法\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "08199949",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<class 'pandas.core.frame.DataFrame'>\n",
      "RangeIndex: 7500 entries, 0 to 7499\n",
      "Data columns (total 31 columns):\n",
      " #   Column                        Non-Null Count  Dtype  \n",
      "---  ------                        --------------  -----  \n",
      " 0   Home Ownership                7500 non-null   int64  \n",
      " 1   Annual Income                 7500 non-null   float64\n",
      " 2   Years in current job          7500 non-null   float64\n",
      " 3   Tax Liens                     7500 non-null   float64\n",
      " 4   Number of Open Accounts       7500 non-null   float64\n",
      " 5   Years of Credit History       7500 non-null   float64\n",
      " 6   Maximum Open Credit           7500 non-null   float64\n",
      " 7   Number of Credit Problems     7500 non-null   float64\n",
      " 8   Months since last delinquent  7500 non-null   float64\n",
      " 9   Bankruptcies                  7500 non-null   float64\n",
      " 10  Long Term                     7500 non-null   int64  \n",
      " 11  Current Loan Amount           7500 non-null   float64\n",
      " 12  Current Credit Balance        7500 non-null   float64\n",
      " 13  Monthly Debt                  7500 non-null   float64\n",
      " 14  Credit Score                  7500 non-null   float64\n",
      " 15  Credit Default                7500 non-null   int64  \n",
      " 16  Purpose_business loan         7500 non-null   int32  \n",
      " 17  Purpose_buy a car             7500 non-null   int32  \n",
      " 18  Purpose_buy house             7500 non-null   int32  \n",
      " 19  Purpose_debt consolidation    7500 non-null   int32  \n",
      " 20  Purpose_educational expenses  7500 non-null   int32  \n",
      " 21  Purpose_home improvements     7500 non-null   int32  \n",
      " 22  Purpose_major purchase        7500 non-null   int32  \n",
      " 23  Purpose_medical bills         7500 non-null   int32  \n",
      " 24  Purpose_moving                7500 non-null   int32  \n",
      " 25  Purpose_other                 7500 non-null   int32  \n",
      " 26  Purpose_renewable energy      7500 non-null   int32  \n",
      " 27  Purpose_small business        7500 non-null   int32  \n",
      " 28  Purpose_take a trip           7500 non-null   int32  \n",
      " 29  Purpose_vacation              7500 non-null   int32  \n",
      " 30  Purpose_wedding               7500 non-null   int32  \n",
      "dtypes: float64(13), int32(15), int64(3)\n",
      "memory usage: 1.3 MB\n"
     ]
    }
   ],
   "source": [
    "# 先运行之前预处理好的代码\n",
    "import pandas as pd\n",
    "import pandas as pd    #用于数据处理和分析，可处理表格数据。\n",
    "import numpy as np     #用于数值计算，提供了高效的数组操作。\n",
    "import matplotlib.pyplot as plt    #用于绘制各种类型的图表\n",
    "import seaborn as sns   #基于matplotlib的高级绘图库，能绘制更美观的统计图形。\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")\n",
    " \n",
    " # 设置中文字体（解决中文显示问题）\n",
    "plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows系统常用黑体字体\n",
    "plt.rcParams['axes.unicode_minus'] = False    # 正常显示负号\n",
    "data = pd.read_csv('data.csv')    #读取数据\n",
    "\n",
    "\n",
    "# 先筛选字符串变量 \n",
    "discrete_features = data.select_dtypes(include=['object']).columns.tolist()\n",
    "# Home Ownership 标签编码\n",
    "home_ownership_mapping = {\n",
    "    'Own Home': 1,\n",
    "    'Rent': 2,\n",
    "    'Have Mortgage': 3,\n",
    "    'Home Mortgage': 4\n",
    "}\n",
    "data['Home Ownership'] = data['Home Ownership'].map(home_ownership_mapping)\n",
    "\n",
    "# Years in current job 标签编码\n",
    "years_in_job_mapping = {\n",
    "    '< 1 year': 1,\n",
    "    '1 year': 2,\n",
    "    '2 years': 3,\n",
    "    '3 years': 4,\n",
    "    '4 years': 5,\n",
    "    '5 years': 6,\n",
    "    '6 years': 7,\n",
    "    '7 years': 8,\n",
    "    '8 years': 9,\n",
    "    '9 years': 10,\n",
    "    '10+ years': 11\n",
    "}\n",
    "data['Years in current job'] = data['Years in current job'].map(years_in_job_mapping)\n",
    "\n",
    "# Purpose 独热编码，记得需要将bool类型转换为数值\n",
    "data = pd.get_dummies(data, columns=['Purpose'])\n",
    "data2 = pd.read_csv(\"data.csv\") # 重新读取数据，用来做列名对比\n",
    "list_final = [] # 新建一个空列表，用于存放独热编码后新增的特征名\n",
    "for i in data.columns:\n",
    "    if i not in data2.columns:\n",
    "       list_final.append(i) # 这里打印出来的就是独热编码后的特征名\n",
    "for i in list_final:\n",
    "    data[i] = data[i].astype(int) # 这里的i就是独热编码后的特征名\n",
    "\n",
    "\n",
    "\n",
    "# Term 0 - 1 映射\n",
    "term_mapping = {\n",
    "    'Short Term': 0,\n",
    "    'Long Term': 1\n",
    "}\n",
    "data['Term'] = data['Term'].map(term_mapping)\n",
    "data.rename(columns={'Term': 'Long Term'}, inplace=True) # 重命名列\n",
    "continuous_features = data.select_dtypes(include=['int64', 'float64']).columns.tolist()  #把筛选出来的列名转换成列表\n",
    " \n",
    " # 连续特征用中位数补全\n",
    "for feature in continuous_features:     \n",
    "    mode_value = data[feature].mode()[0]            #获取该列的众数。\n",
    "    data[feature].fillna(mode_value, inplace=True)          #用众数填充该列的缺失值，inplace=True表示直接在原数据上修改。\n",
    "\n",
    "# 最开始也说了 很多调参函数自带交叉验证，甚至是必选的参数，你如果想要不交叉反而实现起来会麻烦很多\n",
    "# 所以这里我们还是只划分一次数据集\n",
    "\n",
    "data.drop(columns=['Id'], inplace=True) # 删除 Loan ID 列\n",
    "data.info() # 查看数据集的信息，包括数据类型和缺失值情况"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f5a9771e",
   "metadata": {},
   "source": [
    "目前有31个特征，我们看看降维后的特征个数是多少？"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "93de6603",
   "metadata": {},
   "source": [
    "## 特征降维"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0ea45c7",
   "metadata": {},
   "source": [
    "通常情况下，我们提到特征降维，很多时候默认指的是无监督降维，这种方法只需要特征数据本身。但是实际上还包含一种有监督的方法"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "151733ca",
   "metadata": {},
   "source": [
    "\n",
    "1.  **无监督降维 (Unsupervised Dimensionality Reduction)**\n",
    "    *   **定义**：这类算法在降维过程中**不使用**任何关于数据样本的标签信息（比如类别标签、目标值等）。它们仅仅根据数据点本身的分布、方差、相关性、局部结构等特性来寻找低维表示。\n",
    "    *   **输入**：只有特征矩阵 `X`。\n",
    "    *   **目标**：\n",
    "        *   保留数据中尽可能多的方差（如 PCA）。\n",
    "        *   保留数据的局部或全局流形结构（如 LLE, Isomap, t-SNE, UMAP）。\n",
    "        *   找到能够有效重构原始数据的紧凑表示（如 Autoencoder）。\n",
    "        *   找到统计上独立的成分（如 ICA）。\n",
    "    *   **典型算法**：\n",
    "        *   **PCA (Principal Component Analysis)** / **SVD (Singular Value Decomposition)**\n",
    "        *   t-SNE (t-distributed Stochastic Neighbor Embedding)\n",
    "        *   UMAP (Uniform Manifold Approximation and Projection)\n",
    "        *   LLE (Locally Linear Embedding)\n",
    "        *   Isomap (Isometric Mapping)\n",
    "        *   Autoencoders (基本形式)\n",
    "        *   ICA (Independent Component Analysis)\n",
    "    *   **“只需要特征就可以对特征降维了”**：你这句话描述的就是无监督降维。算法通过分析特征间的关系和分布来进行降维。\n",
    "\n",
    "2.  **有监督降维 (Supervised Dimensionality Reduction)**\n",
    "    *   **定义**：这类算法在降维过程中**会利用**数据样本的标签信息（通常是类别标签 `y`）。它们的目标是找到一个低维子空间，在这个子空间中，不同类别的数据点能够被更好地分离开，或者说，这个低维表示更有利于后续的分类（或回归）任务。\n",
    "    *   **输入**：特征矩阵 `X` **和** 对应的标签向量 `y`。\n",
    "    *   **目标**：\n",
    "        *   最大化不同类别之间的可分性，同时最小化同一类别内部的离散度（如 LDA）。\n",
    "        *   找到对预测目标变量 `y` 最有信息量的特征组合。\n",
    "    *   **典型算法**：\n",
    "        *   **LDA (Linear Discriminant Analysis)**：这是最经典的监督降维算法。它寻找的投影方向能够最大化类间散度与类内散度之比。\n",
    "        *   还有一些其他的，比如 NCA (Neighbourhood Components Analysis)，但 LDA 是最主要的代表。\n",
    "    *   **“还需要有分类标签么”**：是的，对于有监督降维，分类标签（或其他形式的监督信号）是必需的。\n",
    "\n",
    "**核心差异总结：**\n",
    "\n",
    "| 特性           | 无监督降维 (Unsupervised DR)      | 有监督降维 (Supervised DR)           |\n",
    "| :------------- | :------------------------------ | :----------------------------------- |\n",
    "| **是否使用标签** | **否** (只使用特征 `X`)         | **是** (使用特征 `X` 和标签 `y`)       |\n",
    "| **主要目的**     | 保留数据固有结构、方差、可视化等  | 提高后续监督学习任务（如分类）的性能 |\n",
    "| **关注点**       | 数据本身的内在属性                | 数据类别间的可分性/与目标变量的相关性 |\n",
    "| **典型例子**     | PCA, SVD, t-SNE, UMAP, Autoencoder | LDA                                  |\n",
    "\n",
    "**举个例子来说明：**\n",
    "\n",
    "*   **PCA (无监督)**：如果你有一堆人脸图片，PCA会尝试找到那些能最好地概括所有人脸变化的“主脸”（特征向量），比如脸型、鼻子大小等，它不关心这些人脸属于谁。\n",
    "*   **LDA (有监督)**：如果你有一堆人脸图片，并且你知道每张图片属于哪个人（标签）。LDA会尝试找到那些能最好地区分不同人的人脸特征组合。比如，如果A和B的脸型很像，但眼睛差别很大，LDA可能会更强调眼睛的特征，即使脸型方差更大。PCA是利用最大化方差来实现无监督降维，而LDA则是在此基础上，加入了类别信息，其优化目标就变成了类间差异最大化和类内差异最小化。\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd5f81cd",
   "metadata": {},
   "source": [
    "PCA等无监督降维方法的目标是保留数据的最大方差，这些方差大的方向不一定是对分类最有用的方向。因此，在分类任务中，LDA通常比PCA更直接有效。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "24feccad",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "X = data.drop(['Credit Default'], axis=1)  # 特征，axis=1表示按列删除\n",
    "y = data['Credit Default'] # 标签\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "5d758a13",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "X = data.drop(['Credit Default'], axis=1)  # 特征，axis=1表示按列删除\n",
    "y = data['Credit Default'] # 标签\n",
    "# 按照8:2划分训练集和测试集\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)  # 80%训练集，20%测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "66260542",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- 1. 默认参数随机森林 (训练集 -> 测试集) ---\n",
      "训练与预测耗时: 1.7385 秒\n",
      "\n",
      "默认随机森林 在测试集上的分类报告：\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.77      0.96      0.85      1059\n",
      "           1       0.76      0.30      0.43       441\n",
      "\n",
      "    accuracy                           0.77      1500\n",
      "   macro avg       0.77      0.63      0.64      1500\n",
      "weighted avg       0.77      0.77      0.73      1500\n",
      "\n",
      "默认随机森林 在测试集上的混淆矩阵：\n",
      "[[1018   41]\n",
      " [ 309  132]]\n"
     ]
    }
   ],
   "source": [
    "\n",
    "from sklearn.ensemble import RandomForestClassifier #随机森林分类器\n",
    "\n",
    "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score # 用于评估分类器性能的指标\n",
    "from sklearn.metrics import classification_report, confusion_matrix #用于生成分类报告和混淆矩阵\n",
    "import warnings #用于忽略警告信息\n",
    "warnings.filterwarnings(\"ignore\") # 忽略所有警告信息\n",
    "# --- 1. 默认参数的随机森林 ---\n",
    "# 评估基准模型，这里确实不需要验证集\n",
    "print(\"--- 1. 默认参数随机森林 (训练集 -> 测试集) ---\")\n",
    "import time # 这里介绍一个新的库，time库，主要用于时间相关的操作，因为调参需要很长时间，记录下会帮助后人知道大概的时长\n",
    "start_time = time.time() # 记录开始时间\n",
    "rf_model = RandomForestClassifier(random_state=42)\n",
    "rf_model.fit(X_train, y_train) # 在训练集上训练\n",
    "rf_pred = rf_model.predict(X_test) # 在测试集上预测\n",
    "end_time = time.time() # 记录结束时间\n",
    "\n",
    "print(f\"训练与预测耗时: {end_time - start_time:.4f} 秒\")\n",
    "print(\"\\n默认随机森林 在测试集上的分类报告：\")\n",
    "print(classification_report(y_test, rf_pred))\n",
    "print(\"默认随机森林 在测试集上的混淆矩阵：\")\n",
    "print(confusion_matrix(y_test, rf_pred))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c997e260",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "beccb9fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "# # umap-learn 是一个用于降维和可视化的库，特别适合处理高维数据。它使用了一种基于流形学习的算法，可以有效地将高维数据嵌入到低维空间中，同时保持数据的局部结构。\n",
    "# !pip install umap-learn -i https://pypi.tuna.tsinghua.edu.cn/simple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "3234e5cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 确保这些库已导入，你的原始代码中可能已经包含了部分\n",
    "import time\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import StandardScaler # 特征缩放\n",
    "from sklearn.decomposition import PCA # 主成分分析\n",
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA # 线性判别分析\n",
    "# UMAP 需要单独安装: pip install umap-learn\n",
    "import umap # 如果安装了 umap-learn，可以这样导入\n",
    "\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "\n",
    "# 你的 X_train, X_test, y_train, y_test 应该已经根据你的代码准备好了\n",
    "# 我们假设你的随机森林模型参数与基准模型一致，以便比较降维效果\n",
    "# rf_params = {'random_state': 42} # 如果你的基准模型有其他参数，也在这里定义\n",
    "# 为了直接比较，我们使用默认的 RandomForestClassifier 参数，除了 random_state\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "73950dcd",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "### 主成分分析 (PCA)\n",
    "\n",
    "在昨天的专题中已经理解了SVD（奇异值分解），那么理解PCA（主成分分析）就会非常直接。实际上，**PCA可以被看作是将SVD应用于经过均值中心化的数据矩阵，并对其结果进行特定解释的一种方法。**\n",
    "\n",
    "**PCA：寻找最大方差方向**\n",
    "\n",
    "主成分分析 (PCA) 的核心思想是识别数据中方差最大的方向（即主成分）。然后，它将数据投影到由这些最重要的主成分构成的新的、维度更低子空间上。这样做的目的是在降低数据维度的同时，尽可能多地保留原始数据中的“信息”（通过方差来衡量）。新的特征（主成分）是原始特征的线性组合，并且它们之间是正交的（不相关）。\n",
    "\n",
    "**PCA 与 SVD 的关系**\n",
    "\n",
    "假设你的数据矩阵是 `X`（行是样本，列是特征）。\n",
    "\n",
    "1.  **步骤 0：均值中心化 (对PCA的解释至关重要)**\n",
    "    *   对于 `X` 中的每一个特征（列），计算其均值。\n",
    "    *   从该特征列的所有值中减去这个均值。我们将这个经过均值中心化处理的矩阵称为 `X_centered`。\n",
    "    *   **为什么要做这一步？** PCA的目标是找到围绕数据均值的最大方差方向。SVD本身不强制要求均值中心化，但为了将其分解结果解释为PCA的主成分，这一步是必需的。\n",
    "\n",
    "2.  **步骤 1：对均值中心化后的数据应用 SVD**\n",
    "    *   对 `X_centered` 进行奇异值分解：\n",
    "        $X_centered = U * S * V^T$\n",
    "    *   其中：\n",
    "        *   `U`：左奇异向量矩阵（列向量相互正交）。它的列是 $X_centered * X_centered^T$ 的特征向量。\n",
    "        *   `S`：奇异值矩阵（一个对角矩阵，对角线上的奇异值 `s_i` 是非负实数，并按降序排列）。这些 `s_i` 是 $X_centered^T * X_centered$ 和 $X_centered * X_centered^T$ 的特征值的平方根。\n",
    "        *   `V`：右奇异向量矩阵（列向量相互正交）。它的列是 $X_centered^T * X_centered$ 的特征向量。**`V` 的这些列向量就是主成分方向。**\n",
    "\n",
    "3.  **步骤 2：在 PCA 的语境下理解SVD的各个组成部分**\n",
    "    *   **主成分方向 (Principal Component Directions / Loadings):** `V` 矩阵的列向量（即右奇异向量）就是主成分 (PCs)。`V` 的第一列是第一主成分（方差最大的方向），第二列是第二主成分（方差次大且与第一主成分正交的方向），以此类推。这些向量告诉你原始特征是如何线性组合形成各个主成分的。\n",
    "    *   **解释的方差 (Variance Explained):** `S` 矩阵中的奇异值与每个对应主成分所能解释的方差量直接相关。具体来说，第 *i* 个主成分解释的方差是 $(s_i^2) / (n-1)$（其中 `s_i` 是 `S` 中的第 *i* 个奇异值，`n` 是样本数量）。更常见的是看解释方差的*比例*：$(s_i^2) / sum(所有 s_j^2)$。\n",
    "    *   **主成分得分 (Principal Component Scores / Transformed Data):** 如果你想将均值中心化后的数据投影到主成分上（即获得在降维后的空间中的新坐标），你可以计算：\n",
    "        $X_projected = X_centered * V$\n",
    "        根据SVD的公式，这等价于：\n",
    "        $X_projected = (U * S * V^T) * V = U * S * (V^T * V) = U * S * I = U * S$\n",
    "        所以，矩阵 `U * S` 给了你每个数据点在新的主成分轴上的“得分”。\n",
    "        如果想将数据降至 `k` 维，你会取 `V` 的前 `k` 列（记作 `V_k`），`U` 的前 `k` 列和 `S` 的左上角 `k x k` 部分（记作 `U_k` 和 `S_k`）。\n",
    "        降维后的数据就是 $X_reduced = X_centered * V_k = U_k * S_k$。\n",
    "\n",
    "**PCA 何时适用？数据是线性还是非线性？**\n",
    "\n",
    "*   **线性性**:\n",
    "    *   **PCA 是一种线性降维方法**。它假设主成分是原始特征的线性组合。\n",
    "    *   它寻找的是一个能够最好地捕捉数据方差的**线性子空间**。\n",
    "    *   如果你的数据的潜在结构是高度非线性的（例如，“瑞士卷”形状、螺旋形），PCA可能无法有效地在低维空间中捕捉这种结构。它可能会将这类结构“压平”或扭曲。\n",
    "\n",
    "*   **PCA 效果好的情况**:\n",
    "    *   **目标是最大化方差**: 当你认为数据中方差最大的方向包含了最重要的信息时。这在去噪或特征间存在相关性时通常是成立的。\n",
    "    *   **数据分布大致呈椭球形或存在线性相关性**: PCA 擅长找到这类分布的主轴。\n",
    "    *   **作为其他线性模型的预处理步骤**: 不相关的主成分有时能让线性模型（如逻辑回归、线性SVM）表现更好。\n",
    "    *   **探索性数据分析 (EDA)**: 快速了解数据变异的主要模式。\n",
    "    *   **降噪**: 假设噪声的方差低于信号的方差，PCA可以通过舍弃低方差的成分来帮助降噪。\n",
    "    *   **当原始特征数量非常多，且存在多重共线性时**：PCA可以通过生成少数几个不相关的主成分来解决多重共线性问题，并减少特征数量。\n",
    "\n",
    "*   **PCA 可能不适用或需要谨慎使用的情况**:\n",
    "    *   **高度非线性数据**: 对于分布在复杂流形上的数据（例如“瑞士卷”、“S型曲线”），PCA会将其投影到一个线性子空间，这可能会丢失关键的非线性关系。在这种情况下，非线性降维技术（如 t-SNE, UMAP, LLE, Isomap, 核PCA, 自编码器）会是更好的选择。\n",
    "    *   **方差并非衡量重要性的唯一标准**: 有时，方差较小的方向可能对特定任务至关重要（例如，在分类问题中，如果使用LDA，一个整体方差较小的方向可能对区分类别非常有效）。PCA是无监督的，它不考虑类别标签。\n",
    "    *   **主成分的可解释性**: 虽然主成分是原始特征的线性组合，但与保留原始、可解释的特征相比，它们的直接物理解释有时可能更具挑战性。\n",
    "    *   **数据特征尺度差异巨大**: 如果特征的尺度（单位或数值范围）相差悬殊（例如，一个特征以米为单位，另一个以毫米为单位），那么尺度较大的特征将在方差计算中占据主导地位，从而主导第一主成分。这就是为什么在应用PCA之前**几乎总是推荐进行数据标准化（例如，将特征缩放到均值为0，方差为1）**。\n",
    "\n",
    "总而言之，可以将PCA视为：\n",
    "1.  对数据进行均值中心化。\n",
    "2.  对中心化后的数据进行SVD。\n",
    "3.  使用SVD得到的右奇异向量 `V` 作为主成分方向。\n",
    "4.  使用奇异值 `S` 来评估每个主成分的重要性（解释的方差）。\n",
    "5.  使用 `U*S`（或 `X_centered * V`）来获得降维后的数据表示。\n",
    "\n",
    "PCA主要适用于那些你认为最重要的信息可以通过数据方差来捕获，并且数据结构主要是线性的情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0957b1a5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- 2. PCA 降维 + 随机森林 (不使用 Pipeline) ---\n",
      "为了保留95%的方差，需要的主成分数量: 26\n"
     ]
    }
   ],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.decomposition import PCA\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "import time\n",
    "import numpy as np # 确保numpy导入\n",
    "\n",
    "# 假设 X_train, X_test, y_train, y_test 已经准备好了\n",
    "\n",
    "print(f\"\\n--- 2. PCA 降维 + 随机森林 (不使用 Pipeline) ---\")\n",
    "\n",
    "\n",
    "# 步骤 1: 特征缩放\n",
    "scaler_pca = StandardScaler()\n",
    "X_train_scaled_pca = scaler_pca.fit_transform(X_train)\n",
    "X_test_scaled_pca = scaler_pca.transform(X_test) # 使用在训练集上fit的scaler\n",
    "\n",
    "# 步骤 2: PCA降维\n",
    "# 选择降到10维，或者你可以根据解释方差来选择，例如：\n",
    "pca_expl = PCA(random_state=42)\n",
    "pca_expl.fit(X_train_scaled_pca)\n",
    "cumsum_variance = np.cumsum(pca_expl.explained_variance_ratio_)\n",
    "n_components_to_keep_95_var = np.argmax(cumsum_variance >= 0.95) + 1\n",
    "print(f\"为了保留95%的方差，需要的主成分数量: {n_components_to_keep_95_var}\")\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "ca2329e2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PCA降维后，训练集形状: (6000, 10), 测试集形状: (1500, 10)\n",
      "手动PCA降维后，训练与预测耗时: 3.7586 秒\n",
      "\n",
      "手动 PCA + 随机森林 在测试集上的分类报告：\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.77      0.94      0.85      1059\n",
      "           1       0.70      0.33      0.45       441\n",
      "\n",
      "    accuracy                           0.76      1500\n",
      "   macro avg       0.74      0.64      0.65      1500\n",
      "weighted avg       0.75      0.76      0.73      1500\n",
      "\n",
      "手动 PCA + 随机森林 在测试集上的混淆矩阵：\n",
      "[[996  63]\n",
      " [295 146]]\n"
     ]
    }
   ],
   "source": [
    "# 我们测试下降低到10维的效果\n",
    "n_components_pca = 10\n",
    "pca_manual = PCA(n_components=n_components_pca, random_state=42)\n",
    "\n",
    "X_train_pca = pca_manual.fit_transform(X_train_scaled_pca)\n",
    "X_test_pca = pca_manual.transform(X_test_scaled_pca) # 使用在训练集上fit的pca\n",
    "\n",
    "print(f\"PCA降维后，训练集形状: {X_train_pca.shape}, 测试集形状: {X_test_pca.shape}\")\n",
    "start_time_pca_manual = time.time()\n",
    "# 步骤 3: 训练随机森林分类器\n",
    "rf_model_pca = RandomForestClassifier(random_state=42)\n",
    "rf_model_pca.fit(X_train_pca, y_train)\n",
    "\n",
    "# 步骤 4: 在测试集上预测\n",
    "rf_pred_pca_manual = rf_model_pca.predict(X_test_pca)\n",
    "end_time_pca_manual = time.time()\n",
    "\n",
    "print(f\"手动PCA降维后，训练与预测耗时: {end_time_pca_manual - start_time_pca_manual:.4f} 秒\")\n",
    "\n",
    "print(\"\\n手动 PCA + 随机森林 在测试集上的分类报告：\")\n",
    "print(classification_report(y_test, rf_pred_pca_manual))\n",
    "print(\"手动 PCA + 随机森林 在测试集上的混淆矩阵：\")\n",
    "print(confusion_matrix(y_test, rf_pred_pca_manual))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58ad6ef6",
   "metadata": {},
   "source": [
    "### t-分布随机邻域嵌入 (t-SNE)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ef5384b",
   "metadata": {},
   "source": [
    "\n",
    "这是一种与PCA截然不同的降维算法，尤其在理解其核心思想和适用场景上。\n",
    "\n",
    "**t-SNE：保持高维数据的局部邻域结构，用于可视化**\n",
    "\n",
    "PCA 的目标是保留数据的全局方差，而 t-SNE 的核心目标是**在高维空间中相似的数据点，在降维后的低维空间中也应该保持相似（即彼此靠近），而不相似的点则应该相距较远。** 它特别擅长于将高维数据集投影到二维或三维空间进行可视化，从而揭示数据中的簇结构或流形结构。---深度学习可视化中很热门\n",
    "\n",
    "\n",
    "**t-SNE 与 PCA/SVD 的主要差异：**\n",
    "\n",
    "| 特性             | PCA/SVD                                        | t-SNE                                                     |\n",
    "| :--------------- | :--------------------------------------------- | :-------------------------------------------------------- |\n",
    "| **类型**         | 线性                                           | **非线性**                                                |\n",
    "| **主要目标**     | 保留全局方差                                   | **保留局部邻域结构，用于可视化**                            |\n",
    "| **关注点**       | 全局结构                                       | **局部结构**，尽可能在低维呈现高维的邻近关系              |\n",
    "| **计算成本**     | 相对较低                                       | **较高**，尤其是对于大数据集                                |\n",
    "| **输出稳定性**   | 确定性（给定数据，结果唯一）                     | **随机性**（优化过程通常有随机初始化，多次运行结果可能略有不同） |\n",
    "| **超参数**       | `n_components`                                   | **`perplexity` (困惑度)**, `n_iter` (迭代次数), `learning_rate` 等 |\n",
    "| **全局结构保留** | 较好（因为它关注方差）                         | 可能较差，**t-SNE 更关注保持局部结构，可能扭曲全局距离**   |\n",
    "| **降维后坐标含义** | 主成分有明确的方差含义                         | **坐标本身没有直接的物理意义**，点之间的相对距离和簇的形状更重要 |\n",
    "| **适用场景**     | 通用降维，去噪，数据压缩，线性模型预处理         | **高维数据可视化**，探索数据中的簇或流形结构                |\n",
    "\n",
    "**何时适合使用 t-SNE？**\n",
    "\n",
    "*   **当你主要目的是可视化高维数据时**：t-SNE 在将复杂的高维数据结构展现在2D或3D图上时非常强大，能帮助你直观地看到数据中可能存在的簇或模式。\n",
    "*   **当数据具有复杂的非线性结构时**：如果数据分布在一个弯曲的流形上，t-SNE 比 PCA 更能捕捉到这种结构。\n",
    "*   **探索性数据分析**：帮助发现数据中未知的群体。\n",
    "\n",
    "**使用 t-SNE 时需要注意的事项：**\n",
    "\n",
    "*   **计算成本高**：对于非常大的数据集（例如几十万甚至上百万样本），t-SNE 的计算会非常慢。通常建议在应用 t-SNE 之前，先用 PCA 将数据降到一个适中的维度（例如50维），这样可以显著加速 t-SNE 的计算并可能改善结果。\n",
    "*   **超参数敏感**：\n",
    "    *   **Perplexity (困惑度)**：这个参数对结果影响较大。常见的取值范围是 5 到 50。较小的困惑度关注非常局部的结构，较大的困惑度则考虑更广泛的邻域。通常需要尝试不同的值。\n",
    "    *   **n_iter (迭代次数)**：需要足够的迭代次数让算法收敛。默认值通常是1000。如果可视化结果看起来还不稳定，可以尝试增加迭代次数。\n",
    "    *   **learning_rate (学习率)**：也可能影响收敛。\n",
    "*   **结果的解释**：\n",
    "    *   **簇的大小和密度在 t-SNE 图中没有直接意义**。t-SNE 会尝试将所有簇展开到相似的密度。不要根据簇在图上的大小来判断原始数据中簇的实际大小或密度。\n",
    "    *   **点之间的距离在全局上没有意义**。两个相距较远的簇，它们之间的距离并不代表它们在原始高维空间中的实际距离。t-SNE 主要保留的是局部邻域关系。\n",
    "    *   **多次运行结果可能不同**：由于优化过程的随机初始化和梯度下降的性质，多次运行 t-SNE 可能会得到略微不同的可视化结果。但好的簇结构通常是稳定的。\n",
    "*   **不适合作为通用的有监督学习预处理步骤**：因为它的目标是可视化和保持局部结构，而不是最大化类别可分性或保留全局方差，所以它通常不直接用于提高分类器性能的降维。LDA 或 PCA (在某些情况下) 更适合这个目的。\n",
    "\n",
    "**总结一下**：\n",
    "\n",
    "t-SNE 是一种强大的非线性降维技术，主要用于高维数据的可视化。它通过在低维空间中保持高维空间中数据点之间的局部相似性（邻域关系）来工作。与PCA关注全局方差不同，t-SNE 更关注局部细节。理解它的超参数（尤其是困惑度）和结果的正确解读方式非常重要。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "a7e2d36c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- 3. t-SNE 降维 + 随机森林  ---\n",
      "       标准 t-SNE 主要用于可视化，直接用于分类器输入可能效果不佳。\n",
      "正在对训练集进行 t-SNE fit_transform...\n",
      "训练集 t-SNE fit_transform 完成，耗时: 9.82 秒\n",
      "正在对测试集进行 t-SNE fit_transform...\n",
      "测试集 t-SNE fit_transform 完成，耗时: 2.14 秒\n",
      "t-SNE降维后，训练集形状: (6000, 2), 测试集形状: (1500, 2)\n",
      "t-SNE降维数据上，随机森林训练与预测耗时: 1.5160 秒\n",
      "t-SNE 总耗时 (包括两次fit_transform和RF): 13.48 秒\n",
      "\n",
      "手动 t-SNE + 随机森林 在测试集上的分类报告：\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.70      0.90      0.79      1059\n",
      "           1       0.22      0.07      0.10       441\n",
      "\n",
      "    accuracy                           0.66      1500\n",
      "   macro avg       0.46      0.48      0.45      1500\n",
      "weighted avg       0.56      0.66      0.59      1500\n",
      "\n",
      "手动 t-SNE + 随机森林 在测试集上的混淆矩阵：\n",
      "[[954 105]\n",
      " [411  30]]\n"
     ]
    }
   ],
   "source": [
    "from sklearn.manifold import TSNE\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "import time\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt # 用于可选的可视化\n",
    "import seaborn as sns # 用于可选的可视化\n",
    "\n",
    "# 假设 X_train, X_test, y_train, y_test 已经准备好了\n",
    "# 并且你的 X_train, X_test 是DataFrame或Numpy Array\n",
    "\n",
    "print(f\"\\n--- 3. t-SNE 降维 + 随机森林  ---\")\n",
    "print(\"       标准 t-SNE 主要用于可视化，直接用于分类器输入可能效果不佳。\")\n",
    "\n",
    "\n",
    "# 步骤 1: 特征缩放\n",
    "scaler_tsne = StandardScaler()\n",
    "X_train_scaled_tsne = scaler_tsne.fit_transform(X_train)\n",
    "X_test_scaled_tsne = scaler_tsne.transform(X_test) # 使用在训练集上fit的scaler\n",
    "\n",
    "# 步骤 2: t-SNE 降维\n",
    "# 我们将降维到与PCA相同的维度（例如10维）或者一个适合分类的较低维度。\n",
    "# t-SNE通常用于2D/3D可视化，但也可以降到更高维度。\n",
    "# 然而，降到与PCA一样的维度（比如10维）对于t-SNE来说可能不是其优势所在，\n",
    "# 并且计算成本会显著增加，因为高维t-SNE的优化更困难。\n",
    "# 为了与PCA的 n_components=10 对比，我们这里也尝试降到10维。\n",
    "# 但请注意，这可能非常耗时，且效果不一定好。\n",
    "# 通常如果用t-SNE做分类的预处理（不常见），可能会选择非常低的维度（如2或3）。\n",
    "\n",
    "# n_components_tsne = 10 # 与PCA的例子保持一致，但计算量会很大\n",
    "n_components_tsne = 2    # 更典型的t-SNE用于分类的维度，如果想快速看到结果\n",
    "                         # 如果你想严格对比PCA的10维，可以将这里改为10，但会很慢\n",
    "\n",
    "\n",
    "\n",
    "# 对训练集进行 fit_transform\n",
    "tsne_model_train = TSNE(n_components=n_components_tsne,\n",
    "                        perplexity=30,    # 常用的困惑度值\n",
    "                        n_iter=1000,      # 足够的迭代次数\n",
    "                        init='pca',       # 使用PCA初始化，通常更稳定\n",
    "                        learning_rate='auto', # 自动学习率 (sklearn >= 1.2)\n",
    "                        random_state=42,  # 保证结果可复现\n",
    "                        n_jobs=-1)        # 使用所有CPU核心\n",
    "print(\"正在对训练集进行 t-SNE fit_transform...\")\n",
    "start_tsne_fit_train = time.time()\n",
    "X_train_tsne = tsne_model_train.fit_transform(X_train_scaled_tsne)\n",
    "end_tsne_fit_train = time.time()\n",
    "print(f\"训练集 t-SNE fit_transform 完成，耗时: {end_tsne_fit_train - start_tsne_fit_train:.2f} 秒\")\n",
    "\n",
    "\n",
    "# 对测试集进行 fit_transform\n",
    "# 再次强调：这是独立于训练集的变换\n",
    "tsne_model_test = TSNE(n_components=n_components_tsne,\n",
    "                       perplexity=30,\n",
    "                       n_iter=1000,\n",
    "                       init='pca',\n",
    "                       learning_rate='auto',\n",
    "                       random_state=42, # 保持参数一致，但数据不同，结果也不同\n",
    "                       n_jobs=-1)\n",
    "print(\"正在对测试集进行 t-SNE fit_transform...\")\n",
    "start_tsne_fit_test = time.time()\n",
    "X_test_tsne = tsne_model_test.fit_transform(X_test_scaled_tsne) # 注意这里是 X_test_scaled_tsne\n",
    "end_tsne_fit_test = time.time()\n",
    "print(f\"测试集 t-SNE fit_transform 完成，耗时: {end_tsne_fit_test - start_tsne_fit_test:.2f} 秒\")\n",
    "\n",
    "print(f\"t-SNE降维后，训练集形状: {X_train_tsne.shape}, 测试集形状: {X_test_tsne.shape}\")\n",
    "\n",
    "start_time_tsne_rf = time.time()\n",
    "# 步骤 3: 训练随机森林分类器\n",
    "rf_model_tsne = RandomForestClassifier(random_state=42)\n",
    "rf_model_tsne.fit(X_train_tsne, y_train)\n",
    "\n",
    "# 步骤 4: 在测试集上预测\n",
    "rf_pred_tsne_manual = rf_model_tsne.predict(X_test_tsne)\n",
    "end_time_tsne_rf = time.time()\n",
    "\n",
    "print(f\"t-SNE降维数据上，随机森林训练与预测耗时: {end_time_tsne_rf - start_time_tsne_rf:.4f} 秒\")\n",
    "total_tsne_time = (end_tsne_fit_train - start_tsne_fit_train) + \\\n",
    "                  (end_tsne_fit_test - start_tsne_fit_test) + \\\n",
    "                  (end_time_tsne_rf - start_time_tsne_rf)\n",
    "print(f\"t-SNE 总耗时 (包括两次fit_transform和RF): {total_tsne_time:.2f} 秒\")\n",
    "\n",
    "\n",
    "print(\"\\n手动 t-SNE + 随机森林 在测试集上的分类报告：\")\n",
    "print(classification_report(y_test, rf_pred_tsne_manual))\n",
    "print(\"手动 t-SNE + 随机森林 在测试集上的混淆矩阵：\")\n",
    "print(confusion_matrix(y_test, rf_pred_tsne_manual))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e13ecb54",
   "metadata": {},
   "source": [
    "\n",
    "### 线性判别分析 (Linear Discriminant Analysis, LDA) \n",
    "\n",
    "**1. 核心定义与目标：**\n",
    "线性判别分析 (LDA) 是一种经典的**有监督**降维算法，也常直接用作分类器。作为降维技术时，其核心目标是找到一个低维特征子空间（即原始特征的线性组合），使得在该子空间中，不同类别的数据点尽可能地分开（**类间距离最大化**），而同一类别的数据点尽可能地聚集（**类内方差最小化**）。\n",
    "\n",
    "**2. 工作原理简述：**\n",
    "LDA 通过最大化“类间散布矩阵”与“类内散布矩阵”之比的某种度量（例如它们的行列式之比）来实现其降维目标。它寻找能够最好地区分已定义类别的投影方向。\n",
    "\n",
    "**3. 关键特性：**\n",
    "\n",
    "*   **有监督性 (Supervised):** 这是 LDA 与 PCA 最根本的区别。LDA 在降维过程中**必须使用数据的类别标签 (y)** 来指导投影方向的选择，目的是优化类别的可分离性。\n",
    "*   **降维目标维度 (Number of Components):** LDA 降维后的维度（即生成的判别特征的数量）有一个严格的上限：**`min(n_features, n_classes - 1)`**。\n",
    "    *   `n_features`：原始特征的数量。\n",
    "    *   `n_classes`：**类别标签 (y) 中不同类别的数量**。\n",
    "    *   这意味着，例如，对于一个二分类问题 (`n_classes = 2`)，LDA 最多能将数据降至 1 维。如果有 5 个类别，最多能降至 4 维（前提是原始特征数不少于4）。这个特性直接源于其优化目标。\n",
    "*   **线性变换 (Linear Transformation):** 与 PCA 类似，LDA 也是一种线性方法。它找到的是原始特征的线性组合来形成新的、具有判别能力的低维特征（称为判别向量或判别成分）。\n",
    "*   **数据假设 (Assumptions):**\n",
    "    *   理论上，LDA 假设每个类别的数据服从多元高斯分布。\n",
    "    *   理论上，LDA 假设所有类别具有相同的协方差矩阵。\n",
    "    *   在实践中，即使这些假设不完全满足，LDA 通常也能表现良好，尤其是在类别大致呈椭球状分布且大小相似时。\n",
    "\n",
    "**4. 输入要求：**\n",
    "\n",
    "*   **特征 (X):** 数值型特征。如果存在类别型特征，通常需要先进行预处理（如独热编码）。\n",
    "*   **标签 (y):** **一维的、代表类别身份的数组或 Series** (例如 `[0, 1, 0, 2, 1]`)。LDA **不需要**标签进行独热编码。标签的类别数量直接决定了降维的上限。\n",
    "\n",
    "**5. 与特征 (X) 和标签 (y) 的关系：**\n",
    "\n",
    "*   LDA 的降维过程和结果**直接由标签 `y` 中的类别结构驱动**。它试图找到最能区分这些由 `y` 定义的类别的特征组合。\n",
    "*   原始特征 `X` 提供了构建这些判别特征的原材料。特征 `X` 的质量和相关性会影响 LDA 的效果，但降维的“方向盘”是由 `y` 控制的。\n",
    "\n",
    "**6. 优点：**\n",
    "\n",
    "*   直接优化类别可分性，非常适合作为分类任务的预处理步骤，往往能提升后续分类器的性能。\n",
    "*   计算相对高效。\n",
    "*   生成的低维特征具有明确的判别意义。\n",
    "\n",
    "**7. 局限性与注意事项：**\n",
    "\n",
    "*   降维的维度受限于 `n_classes - 1`，这可能比 PCA 能达到的降维程度低很多，尤其是在类别数较少时。\n",
    "*   作为线性方法，可能无法捕捉数据中非线性的类别结构。如果类别边界是非线性的，LDA 效果可能不佳。\n",
    "*   对数据的高斯分布和等协方差假设在理论上是存在的，极端偏离这些假设可能影响性能。\n",
    "*   如果类别在原始特征空间中本身就高度重叠，LDA 的区分能力也会受限。\n",
    "\n",
    "**8. 适用场景：**\n",
    "\n",
    "*   当目标是**提高后续分类模型的性能**时，LDA 是一个强有力的降维工具。\n",
    "*   当类别信息已知且被认为是区分数据的主要因素时。\n",
    "*   当希望获得具有良好类别区分性的低维表示时，尤其可用于数据可视化（如果能降到2D或3D）。\n",
    "\n",
    "**简而言之，LDA 是一种利用类别标签信息来寻找最佳类别分离投影的降维方法，其降维的潜力直接与类别数量挂钩。**\n",
    "\n",
    "---\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "ed2bacb4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- 4. LDA 降维 + 随机森林 ---\n",
      "原始特征数: 30, 类别数: 2\n",
      "LDA 最多可降至 1 维。\n",
      "目标降维维度: 10 维。\n",
      "本次 LDA 将实际降至 1 维。\n",
      "LDA降维后，训练集形状: (6000, 1), 测试集形状: (1500, 1)\n",
      "LDA降维数据上，随机森林训练与预测耗时: 1.8556 秒\n",
      "\n",
      "手动 LDA + 随机森林 在测试集上的分类报告：\n",
      "              precision    recall  f1-score   support\n",
      "\n",
      "           0       0.78      0.78      0.78      1059\n",
      "           1       0.47      0.47      0.47       441\n",
      "\n",
      "    accuracy                           0.69      1500\n",
      "   macro avg       0.63      0.63      0.63      1500\n",
      "weighted avg       0.69      0.69      0.69      1500\n",
      "\n",
      "手动 LDA + 随机森林 在测试集上的混淆矩阵：\n",
      "[[828 231]\n",
      " [233 208]]\n"
     ]
    }
   ],
   "source": [
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import classification_report, confusion_matrix\n",
    "import time\n",
    "import numpy as np\n",
    "# 假设你已经导入了 matplotlib 和 seaborn 用于绘图 (如果需要)\n",
    "import matplotlib.pyplot as plt\n",
    "from mpl_toolkits.mplot3d import Axes3D # 如果需要3D绘图\n",
    "import seaborn as sns\n",
    "\n",
    "\n",
    "\n",
    "print(f\"\\n--- 4. LDA 降维 + 随机森林 ---\")\n",
    "\n",
    "# 步骤 1: 特征缩放\n",
    "scaler_lda = StandardScaler()\n",
    "X_train_scaled_lda = scaler_lda.fit_transform(X_train)\n",
    "X_test_scaled_lda = scaler_lda.transform(X_test) # 使用在训练集上fit的scaler\n",
    "\n",
    "# 步骤 2: LDA 降维\n",
    "n_features = X_train_scaled_lda.shape[1]\n",
    "if hasattr(y_train, 'nunique'):\n",
    "    n_classes = y_train.nunique()\n",
    "elif isinstance(y_train, np.ndarray):\n",
    "    n_classes = len(np.unique(y_train))\n",
    "else:\n",
    "    n_classes = len(set(y_train))\n",
    "\n",
    "max_lda_components = min(n_features, n_classes - 1)\n",
    "\n",
    "# 设置目标降维维度\n",
    "n_components_lda_target = 10\n",
    "\n",
    "if max_lda_components < 1:\n",
    "    print(f\"LDA 不适用，因为类别数 ({n_classes})太少，无法产生至少1个判别组件。\")\n",
    "    X_train_lda = X_train_scaled_lda.copy() # 使用缩放后的原始特征\n",
    "    X_test_lda = X_test_scaled_lda.copy()   # 使用缩放后的原始特征\n",
    "    actual_n_components_lda = n_features\n",
    "    print(\"将使用缩放后的原始特征进行后续操作。\")\n",
    "else:\n",
    "    # 实际使用的组件数不能超过LDA的上限，也不能超过我们的目标（如果目标更小）\n",
    "    actual_n_components_lda = min(n_components_lda_target, max_lda_components)\n",
    "    if actual_n_components_lda < 1: # 这种情况理论上不会发生，因为上面已经检查了 max_lda_components < 1\n",
    "        print(f\"计算得到的实际LDA组件数 ({actual_n_components_lda}) 小于1，LDA不适用。\")\n",
    "        X_train_lda = X_train_scaled_lda.copy()\n",
    "        X_test_lda = X_test_scaled_lda.copy()\n",
    "        actual_n_components_lda = n_features\n",
    "        print(\"将使用缩放后的原始特征进行后续操作。\")\n",
    "    else:\n",
    "        print(f\"原始特征数: {n_features}, 类别数: {n_classes}\")\n",
    "        print(f\"LDA 最多可降至 {max_lda_components} 维。\")\n",
    "        print(f\"目标降维维度: {n_components_lda_target} 维。\")\n",
    "        print(f\"本次 LDA 将实际降至 {actual_n_components_lda} 维。\")\n",
    "\n",
    "        lda_manual = LinearDiscriminantAnalysis(n_components=actual_n_components_lda, solver='svd')\n",
    "        X_train_lda = lda_manual.fit_transform(X_train_scaled_lda, y_train)\n",
    "        X_test_lda = lda_manual.transform(X_test_scaled_lda)\n",
    "\n",
    "print(f\"LDA降维后，训练集形状: {X_train_lda.shape}, 测试集形状: {X_test_lda.shape}\")\n",
    "\n",
    "start_time_lda_rf = time.time()\n",
    "# 步骤 3: 训练随机森林分类器\n",
    "rf_model_lda = RandomForestClassifier(random_state=42)\n",
    "rf_model_lda.fit(X_train_lda, y_train)\n",
    "\n",
    "# 步骤 4: 在测试集上预测\n",
    "rf_pred_lda_manual = rf_model_lda.predict(X_test_lda)\n",
    "end_time_lda_rf = time.time()\n",
    "\n",
    "print(f\"LDA降维数据上，随机森林训练与预测耗时: {end_time_lda_rf - start_time_lda_rf:.4f} 秒\")\n",
    "\n",
    "print(\"\\n手动 LDA + 随机森林 在测试集上的分类报告：\")\n",
    "print(classification_report(y_test, rf_pred_lda_manual))\n",
    "print(\"手动 LDA + 随机森林 在测试集上的混淆矩阵：\")\n",
    "print(confusion_matrix(y_test, rf_pred_lda_manual))\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "vs",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
